// Copyright 2012 Google Inc. All Rights Reserved.

package com.google.common.labs.fsm;

import com.google.common.labs.fsm.ScxmlDoc.ParseException;
import com.google.common.labs.fsm.ScxmlDoc.State;
import com.google.common.labs.fsm.ScxmlDoc.Transition;

import java.io.IOException;
import java.util.Collection;

/**
 * The Scxml2Java produces .java source code for an abstract base class of a
 * finite state machine described by an @{link ScxmlDoc} object
 */
public class Scxml2Java {

  private final ScxmlDoc mDoc;


  private Scxml2Java(ScxmlDoc doc) {
    mDoc = doc;
  }

  public static Scxml2Java translatorForScxml(ScxmlDoc doc) {
    Scxml2Java translator = new Scxml2Java(doc);
    return translator;
  }

  private void out(String s) {
    System.out.print(s + "\n");
  }


  private void out(int tabs, String s) {
    for (int i = 0; i < tabs; i++) {
      s = "    " + s;
    }
    out(s);
  }

  /**
   * Generate Java enum for a collection of strings
   */

  private void outputEnum(String name, Collection<String> strings) {
    out(1, "public enum " + name + " {");
    for (String s : strings) {
      out(2, s + ",");
    }
    out(1, "}\n");
  }

  private void outputExceptionDeclaration(String string) {
    out(1, "public class " + string + " extends RuntimeException {");
    out(2, "public " + string + "(String message) {");
    out(3, "super(message);");
    out(2, "}");
    out(1, "}");
  }

  private void outputIsInTerminalState() {
    out(1, "public boolean isInTerminalState() {");
    for (State s : mDoc.getTerminalStates()) {
      out(2, "if (mCurrentState == State." + s.mId + ") return true;");
    }

    out(2, "return false;");
    out(1, "}\n");
  }

  private void outputDebugLog(int i, String which, String msg) {
    if (mDoc.mDebug) {
      out(i, "if (mDebugLogger != null) mDebugLogger." + which + "(" + msg + ");");
    }
  }

  private void outputStartMethods() {
    out(1, "public void start() {");
    out(2, "start(State." + mDoc.getInitialStateName() + ");");
    out(1, "}\n");
    out(1, "public void start(State state) {");
    out(2, "if (mCurrentState != null) {");
    out(3, "throw new StateException(\"FSM already started.\");");
    out(2, "}");
    out(2, "transitionToState(state);");
    out(1, "}\n");
  }

  private void outputHandleBaseEvent() {
    out(1, "private boolean handleBaseEvent(Event event) {");
    outputEventSwitch(2, mDoc.getBaseState(), false);
    out(2, "return true;");
    out(1, "}\n");
  }

  private void outputEventSwitch(int i, State state, boolean callBase) {
    out(i, "switch (event) {");
    for (Transition transition : state.mTransitions) {
      out(i + 1, "case " + transition.mEvent + ":");
      for (String action : transition.mActions) {
        outputDebugLog(i + 2, "onAction", "\"" + action + "\"");
        out(i + 2, "onAction" + action + "();");
      }
      out(i + 2, "transitionToState(State." + transition.mTarget + ");");
      out(i + 2, "break;");
    }
    out(i + 1, "default:");

    if (callBase) {
      out(i + 2, "if (!handleBaseEvent(event)) {");
      out(i + 3,
          "throw new StateException(\"State." + state.mId + " does not handle Event.\" + event);");
      out(i + 2, "}");
    } else {
      out(i + 2, "mPushingEvent = null;");
      out(i + 2,
          "throw new StateException(\"State." + state.mId + " does not handle Event.\" + event);");
    }
    out(i, "}");
  }

  private void outputPushEventMethod() {
    out(1, "public final boolean pushEvent(Event event) {");

    outputDebugLog(2, "onEvent", "event.toString()");
    out(2, "if (mPushingEvent != null) {");

    out(3,
        "throw new StateException(\"pushEvent(\"+event+\") called during push of \"+mPushingEvent);"
        );
    out(2, "}");
    out(2, "mPushingEvent = event;");

    out(2, "switch (mCurrentState) {");
    for (State state : mDoc.getStates()) {

      if ((mDoc.getBaseState() != null) && mDoc.getBaseState().mId.equals(state.mId)) {
        continue;
      }
      out(3, "case " + state.mId + ":");
      if (state.getTransitions().size() == 0) {
        out(4, "return false;");
        continue;
      }
      outputEventSwitch(4, state, mDoc.getBaseState() != null);
      out(4, "break;");
    }
    out(3, "default:");
    out(4, "throw new EventException(\"Unknown Event: \"+mCurrentState);");
    out(2, "}");
    out(2, "mPushingEvent = null;");
    out(2, "return true;");
    out(1, "}\n");
  }

  /**
   * Generate the Java code
   */

  void outputJava() {
    out("// Generated by Scxml2Java -- DO NOT EDIT");
    out("// (edits will be overwritten automatically when scxml is compiled)\n");

    if (getPackageName() != null) {
      out("package " + getPackageName() + ";\n");
    }

    out("public abstract class " + getClassName() + " {\n");

    // Enums
    outputEnum("Event", mDoc.getEventSet());
    outputEnum("State", mDoc.mDeclaredStateNames);

    // Exceptions
    outputExceptionDeclaration("StateException");
    outputExceptionDeclaration("EventException");

    // Interface
    out(1, "public interface DebugLogger {");
    out(2, "public void onState(String msg);");
    out(2, "public void onAction(String msg);");
    out(2, "public void onEvent(String msg);");
    out(1, "}");

    // Fields
    out(1, "private State mCurrentState;");
    out(1, "private Event mPushingEvent;");
    out(1, "private DebugLogger mDebugLogger;");
    out("");

    // Methods
    out(1, "public " + getClassName() + "() {}\n");
    out(1, "public State getCurrentState() {");
    out(2, "return mCurrentState;");
    out(1, "}\n");

    outputStartMethods();

    if (mDoc.getBaseState() != null) {
      outputHandleBaseEvent();
    }
    outputPushEventMethod();
    outputIsInTerminalState();

    out(1, "public void setDebugLogger(DebugLogger logger) {");
    out(2, "mDebugLogger = logger;");
    out(1, "}\n");

    out(1, "private void transitionToState(State state) {");
    out(2, "mCurrentState = state;");
    outputDebugLog(2, "onState", "state.toString()");
    out(2, "onStateChange(state);");

    out(1, "}\n");

    // Abstract methods
    out(1, "protected abstract void onStateChange(State state);\n");
    // Actions
    for (String action : mDoc.getActionSet()) {
      out(1, "protected abstract void onAction" + action + "();");
    }

    out("}");

  }



  /**
   * @return the translated class name
   */
  public String getClassName() {
    return mDoc.getClassName();
  }


  /**
   * @return the name of the package of the generated java class
   */
  public String getPackageName() {
    return mDoc.getPackageName();
  }


  /**
   * @param newClassName the new class name
   */
  public void changeClassName(String newClassName) {
    mDoc.changeClassName(newClassName);
  }


  /**
   * @param args The scxml input file
   * @throws IOException
   * @throws ParseException
   */
  public static void main(String[] args) throws IOException, ParseException {
    ScxmlDoc doc = ScxmlDoc.createFromFile(args[0]);
    Scxml2Java translator = Scxml2Java.translatorForScxml(doc);
    translator.outputJava();
  }

  /**
   * @return a parsed version of the Scxml document
   */
  public ScxmlDoc getDoc() {
    return mDoc;
  }

}
